3.1 同构应用原理
3.1.1 同构概念深入理解
同构(Isomorphic)应用是指同一套代码可以在服务端和客户端运行的应用程序。在Vue SSR中,这意味着Vue组件既可以在Node.js环境中渲染成HTML字符串,也可以在浏览器中正常运行。
// 同构应用的核心思想
const isomorphicConcept = {
'一套代码': {
description: '相同的Vue组件代码',
serverSide: '在Node.js中执行,生成HTML',
clientSide: '在浏览器中执行,提供交互'
},
'两种环境': {
server: {
environment: 'Node.js',
purpose: '预渲染HTML',
output: 'HTML字符串 + 初始状态'
},
client: {
environment: 'Browser',
purpose: '激活交互',
input: 'HTML + 初始状态'
}
}
}
3.1.2 环境差异处理
// src/utils/env.js
// 环境检测工具
export const isServer = typeof window === 'undefined'
export const isClient = typeof window !== 'undefined'
// 安全的DOM操作
export function safeDocument() {
return isClient ? document : null
}
export function safeWindow() {
return isClient ? window : null
}
// 条件执行
export function clientOnly(fn) {
if (isClient) {
return fn()
}
return null
}
export function serverOnly(fn) {
if (isServer) {
return fn()
}
return null
}
3.1.3 通用组件编写
<!-- src/components/UniversalComponent.vue -->
<template>
<div class="universal-component">
<h2>{{ title }}</h2>
<p>当前时间: {{ currentTime }}</p>
<p>环境: {{ environment }}</p>
<button v-if="isClient" @click="updateTime">
更新时间
</button>
</div>
</template>
<script>
import { ref, onMounted } from 'vue'
import { isClient, isServer } from '@/utils/env'
export default {
name: 'UniversalComponent',
props: {
title: {
type: String,
default: '通用组件'
}
},
setup() {
const currentTime = ref(new Date().toLocaleString())
const environment = isServer ? 'Server' : 'Client'
const updateTime = () => {
currentTime.value = new Date().toLocaleString()
}
// 只在客户端执行
onMounted(() => {
console.log('组件已在客户端挂载')
})
return {
currentTime,
environment,
updateTime,
isClient
}
}
}
</script>
3.2 Vue SSR渲染流程
3.2.1 服务端渲染流程
// server/render.js
import { renderToString } from 'vue/server-renderer'
import { createApp } from '../src/app.js'
export async function renderPage(context) {
// 1. 创建应用实例
const { app, router, store } = createApp()
// 2. 设置路由
await router.push(context.url)
await router.isReady()
// 3. 检查路由匹配
const matchedRoute = router.currentRoute.value
if (!matchedRoute.matched.length) {
throw new Error('404 - Page Not Found')
}
// 4. 预取数据
await prefetchData(matchedRoute, store)
// 5. 渲染应用为HTML字符串
const html = await renderToString(app)
// 6. 获取初始状态
const state = store.state
return {
html,
state,
title: matchedRoute.meta?.title || 'Vue SSR App'
}
}
// 数据预取函数
async function prefetchData(route, store) {
const matchedComponents = route.matched
.flatMap(record => Object.values(record.components || {}))
const asyncDataPromises = matchedComponents
.filter(component => component.asyncData)
.map(component => component.asyncData({
store,
route
}))
await Promise.all(asyncDataPromises)
}
3.2.2 客户端激活流程
// src/entry-client.js
import { createApp } from './app.js'
const { app, router, store } = createApp()
// 恢复服务端状态
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__)
}
// 等待路由准备就绪
router.isReady().then(() => {
// 添加路由钩子,处理客户端导航
router.beforeResolve(async (to, from, next) => {
const matched = router.resolve(to).matched
const prevMatched = router.resolve(from).matched
// 找出需要预取数据的组件
let diffed = false
const activated = matched.filter((c, i) => {
return diffed || (diffed = (prevMatched[i] !== c))
})
if (!activated.length) {
return next()
}
// 显示加载指示器
showLoadingIndicator()
try {
// 预取数据
await Promise.all(
activated
.flatMap(record => Object.values(record.components || {}))
.filter(component => component.asyncData)
.map(component => component.asyncData({
store,
route: to
}))
)
hideLoadingIndicator()
next()
} catch (error) {
hideLoadingIndicator()
next(error)
}
})
// 挂载应用
app.mount('#app')
})
function showLoadingIndicator() {
// 显示加载动画
console.log('Loading...')
}
function hideLoadingIndicator() {
// 隐藏加载动画
console.log('Loaded')
}
3.3 状态管理与同步
3.3.1 Vuex状态管理
// src/store/index.js
import { createStore } from 'vuex'
import { userModule } from './modules/user'
import { postsModule } from './modules/posts'
export function createStore() {
return createStore({
modules: {
user: userModule,
posts: postsModule
},
strict: process.env.NODE_ENV !== 'production'
})
}
// src/store/modules/user.js
export const userModule = {
namespaced: true,
state: () => ({
currentUser: null,
isAuthenticated: false,
profile: null
}),
mutations: {
SET_USER(state, user) {
state.currentUser = user
state.isAuthenticated = !!user
},
SET_PROFILE(state, profile) {
state.profile = profile
},
CLEAR_USER(state) {
state.currentUser = null
state.isAuthenticated = false
state.profile = null
}
},
actions: {
async fetchUser({ commit }, userId) {
try {
const response = await fetch(`/api/users/${userId}`)
const user = await response.json()
commit('SET_USER', user)
return user
} catch (error) {
console.error('Failed to fetch user:', error)
throw error
}
},
async fetchProfile({ commit }, userId) {
try {
const response = await fetch(`/api/users/${userId}/profile`)
const profile = await response.json()
commit('SET_PROFILE', profile)
return profile
} catch (error) {
console.error('Failed to fetch profile:', error)
throw error
}
}
},
getters: {
isLoggedIn: state => state.isAuthenticated,
userName: state => state.currentUser?.name || 'Guest',
userAvatar: state => state.profile?.avatar || '/default-avatar.png'
}
}
3.3.2 状态序列化与反序列化
// src/utils/state.js
import serialize from 'serialize-javascript'
// 序列化状态(服务端)
export function serializeState(state) {
return serialize(state, { isJSON: true })
}
// 反序列化状态(客户端)
export function deserializeState(serializedState) {
try {
return JSON.parse(serializedState)
} catch (error) {
console.error('Failed to deserialize state:', error)
return {}
}
}
// 状态注入HTML模板
export function injectState(html, state) {
const serializedState = serializeState(state)
const stateScript = `
<script>
window.__INITIAL_STATE__ = ${serializedState}
</script>
`
return html.replace('</body>', `${stateScript}</body>`)
}
3.3.3 数据预取策略
// src/mixins/asyncData.js
export const asyncDataMixin = {
beforeMount() {
const { asyncData } = this.$options
if (asyncData) {
// 客户端路由切换时预取数据
this.dataPromise = asyncData({
store: this.$store,
route: this.$route
})
}
}
}
// 使用示例
// src/views/UserProfile.vue
export default {
name: 'UserProfile',
mixins: [asyncDataMixin],
// 服务端和客户端都会调用
async asyncData({ store, route }) {
const userId = route.params.id
await Promise.all([
store.dispatch('user/fetchUser', userId),
store.dispatch('user/fetchProfile', userId)
])
},
computed: {
user() {
return this.$store.state.user.currentUser
},
profile() {
return this.$store.state.user.profile
}
}
}
3.4 路由配置与管理
3.4.1 路由器创建
// src/router/index.js
import { createRouter, createWebHistory, createMemoryHistory } from 'vue-router'
import { isServer } from '@/utils/env'
// 路由配置
const routes = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue'),
meta: {
title: '首页',
description: '欢迎来到我们的网站'
}
},
{
path: '/about',
name: 'About',
component: () => import('@/views/About.vue'),
meta: {
title: '关于我们',
description: '了解更多关于我们的信息'
}
},
{
path: '/users/:id',
name: 'UserProfile',
component: () => import('@/views/UserProfile.vue'),
meta: {
title: '用户资料',
requiresAuth: true
}
},
{
path: '/posts',
name: 'Posts',
component: () => import('@/views/Posts.vue'),
meta: {
title: '文章列表'
}
},
{
path: '/posts/:id',
name: 'PostDetail',
component: () => import('@/views/PostDetail.vue'),
meta: {
title: '文章详情'
}
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/views/NotFound.vue'),
meta: {
title: '页面未找到'
}
}
]
// 创建路由器
export function createRouter() {
return createRouter({
history: isServer ? createMemoryHistory() : createWebHistory(),
routes
})
}
3.4.2 路由守卫
// src/router/guards.js
export function setupRouterGuards(router, store) {
// 全局前置守卫
router.beforeEach(async (to, from, next) => {
// 检查认证
if (to.meta.requiresAuth && !store.getters['user/isLoggedIn']) {
next({ name: 'Login', query: { redirect: to.fullPath } })
return
}
// 设置页面标题
if (to.meta.title) {
document.title = `${to.meta.title} - Vue SSR App`
}
next()
})
// 全局后置钩子
router.afterEach((to, from) => {
// 页面访问统计
if (typeof gtag !== 'undefined') {
gtag('config', 'GA_MEASUREMENT_ID', {
page_path: to.fullPath
})
}
})
}
3.4.3 动态路由
// src/router/dynamic.js
export async function addDynamicRoutes(router, store) {
try {
// 从API获取动态路由配置
const response = await fetch('/api/routes')
const dynamicRoutes = await response.json()
dynamicRoutes.forEach(route => {
router.addRoute({
path: route.path,
name: route.name,
component: () => import(`@/views/${route.component}.vue`),
meta: route.meta || {}
})
})
console.log('Dynamic routes added successfully')
} catch (error) {
console.error('Failed to add dynamic routes:', error)
}
}
3.5 组件生命周期处理
3.5.1 SSR生命周期钩子
// SSR环境下的生命周期执行情况
const ssrLifecycleHooks = {
'服务端执行': [
'beforeCreate',
'created'
],
'客户端执行': [
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeUnmount',
'unmounted'
],
'注意事项': [
'mounted钩子不在服务端执行',
'避免在created中访问DOM',
'定时器需要在客户端清理'
]
}
3.5.2 生命周期最佳实践
<!-- src/components/LifecycleDemo.vue -->
<template>
<div class="lifecycle-demo">
<h3>生命周期演示</h3>
<p>组件ID: {{ componentId }}</p>
<p>挂载时间: {{ mountTime }}</p>
<p>计数器: {{ counter }}</p>
<button @click="increment">增加</button>
</div>
</template>
<script>
import { ref, onMounted, onUnmounted } from 'vue'
import { isClient } from '@/utils/env'
export default {
name: 'LifecycleDemo',
setup() {
const componentId = ref(Math.random().toString(36).substr(2, 9))
const mountTime = ref(null)
const counter = ref(0)
let timer = null
const increment = () => {
counter.value++
}
// 只在客户端执行
onMounted(() => {
if (isClient) {
mountTime.value = new Date().toLocaleString()
// 启动定时器
timer = setInterval(() => {
console.log('Timer tick:', counter.value)
}, 5000)
console.log('Component mounted on client')
}
})
// 清理定时器
onUnmounted(() => {
if (timer) {
clearInterval(timer)
timer = null
}
})
return {
componentId,
mountTime,
counter,
increment
}
}
}
</script>
3.5.3 异步组件处理
// src/components/AsyncComponentWrapper.vue
import { defineAsyncComponent } from 'vue'
import LoadingComponent from './LoadingComponent.vue'
import ErrorComponent from './ErrorComponent.vue'
export default {
name: 'AsyncComponentWrapper',
components: {
AsyncComponent: defineAsyncComponent({
loader: () => import('./HeavyComponent.vue'),
loadingComponent: LoadingComponent,
errorComponent: ErrorComponent,
delay: 200,
timeout: 3000,
suspensible: false
})
},
template: `
<div class="async-wrapper">
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<div>Loading async component...</div>
</template>
</Suspense>
</div>
`
}
3.6 错误处理与调试
3.6.1 服务端错误处理
// server/errorHandler.js
export function createErrorHandler() {
return (err, req, res, next) => {
console.error('SSR Error:', err)
// 根据错误类型返回不同响应
if (err.code === 404) {
res.status(404).send(`
<!DOCTYPE html>
<html>
<head><title>页面未找到</title></head>
<body>
<h1>404 - 页面未找到</h1>
<p>抱歉,您访问的页面不存在。</p>
<a href="/">返回首页</a>
</body>
</html>
`)
} else if (err.code === 500 || !err.code) {
res.status(500).send(`
<!DOCTYPE html>
<html>
<head><title>服务器错误</title></head>
<body>
<h1>500 - 服务器内部错误</h1>
<p>服务器遇到了一个错误,请稍后再试。</p>
<a href="/">返回首页</a>
</body>
</html>
`)
} else {
next(err)
}
}
}
3.6.2 客户端错误处理
// src/utils/errorHandler.js
export function setupErrorHandling(app) {
// Vue错误处理
app.config.errorHandler = (err, instance, info) => {
console.error('Vue Error:', err)
console.error('Component:', instance)
console.error('Info:', info)
// 发送错误报告
reportError({
error: err.message,
stack: err.stack,
component: instance?.$options.name,
info
})
}
// 全局未捕获错误
if (typeof window !== 'undefined') {
window.addEventListener('error', (event) => {
console.error('Global Error:', event.error)
reportError({
error: event.error.message,
stack: event.error.stack,
filename: event.filename,
lineno: event.lineno,
colno: event.colno
})
})
// Promise未捕获错误
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled Promise Rejection:', event.reason)
reportError({
error: 'Unhandled Promise Rejection',
reason: event.reason
})
})
}
}
function reportError(errorInfo) {
// 发送错误报告到监控服务
if (process.env.NODE_ENV === 'production') {
fetch('/api/errors', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
...errorInfo,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
url: window.location.href
})
}).catch(err => {
console.error('Failed to report error:', err)
})
}
}
3.6.3 调试工具配置
// src/utils/debug.js
export function setupDebugTools() {
if (process.env.NODE_ENV === 'development' && typeof window !== 'undefined') {
// 添加调试信息到window对象
window.__VUE_SSR_DEBUG__ = {
version: process.env.VUE_APP_VERSION,
buildTime: process.env.VUE_APP_BUILD_TIME,
environment: process.env.NODE_ENV
}
// 性能监控
if (window.performance) {
window.addEventListener('load', () => {
setTimeout(() => {
const perfData = window.performance.timing
const loadTime = perfData.loadEventEnd - perfData.navigationStart
console.log(`Page load time: ${loadTime}ms`)
// 发送性能数据
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/performance', JSON.stringify({
loadTime,
domContentLoaded: perfData.domContentLoadedEventEnd - perfData.navigationStart,
firstPaint: performance.getEntriesByType('paint')[0]?.startTime
}))
}
}, 0)
})
}
}
}
3.7 性能优化基础
3.7.1 组件缓存
// src/utils/componentCache.js
import LRU from 'lru-cache'
// 创建组件缓存
const componentCache = new LRU({
max: 1000,
ttl: 1000 * 60 * 15 // 15分钟
})
export function getCachedComponent(key) {
return componentCache.get(key)
}
export function setCachedComponent(key, component) {
componentCache.set(key, component)
}
export function clearComponentCache() {
componentCache.clear()
}
// 缓存装饰器
export function withCache(component, cacheKey) {
return {
...component,
__cacheKey: cacheKey,
__cached: true
}
}
3.7.2 资源预加载
// src/utils/preload.js
export function preloadRoute(routePath) {
const router = this.$router
const route = router.resolve(routePath)
if (route.matched.length) {
route.matched.forEach(record => {
Object.values(record.components || {}).forEach(component => {
if (typeof component === 'function') {
// 预加载异步组件
component()
}
})
})
}
}
export function preloadResource(href, as = 'script') {
if (typeof document !== 'undefined') {
const link = document.createElement('link')
link.rel = 'preload'
link.href = href
link.as = as
document.head.appendChild(link)
}
}
// 智能预加载
export function setupIntelligentPreloading(router) {
if (typeof window !== 'undefined' && 'IntersectionObserver' in window) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const link = entry.target
const href = link.getAttribute('href')
if (href && href.startsWith('/')) {
preloadRoute(href)
observer.unobserve(link)
}
}
})
})
// 观察所有内部链接
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('a[href^="/"]').forEach(link => {
observer.observe(link)
})
})
}
}
3.8 本章小结
3.8.1 核心概念回顾
- 同构应用:一套代码,两端运行,需要处理环境差异
- 渲染流程:服务端预渲染HTML,客户端激活交互
- 状态管理:服务端和客户端状态同步,数据预取策略
- 路由管理:支持服务端和客户端路由,动态路由配置
- 生命周期:理解SSR环境下的组件生命周期差异
3.8.2 最佳实践
- 使用环境检测避免服务端DOM操作
- 合理设计数据预取策略
- 实现完善的错误处理机制
- 配置组件缓存提升性能
- 使用智能预加载优化用户体验
3.8.3 下章预告
下一章我们将深入学习路由与导航的高级特性: - 嵌套路由配置 - 路由懒加载 - 导航守卫详解 - 动态路由匹配 - 路由元信息应用
练习作业:
- 创建一个包含数据预取的页面组件
- 实现一个通用的错误处理组件
- 配置路由守卫,实现权限控制
- 添加组件缓存,测试性能提升效果
下一章: 路由与导航